package com.atguigu.tingshu.user.service.impl;

import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.WxMaUserService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.album.client.AlbumInfoFeignClient;
import com.atguigu.tingshu.album.client.TrackInfoFeignClient;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.atguigu.tingshu.common.rabbit.service.RabbitService;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.common.util.MongoUtil;
import com.atguigu.tingshu.model.user.*;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.mapper.UserPaidAlbumMapper;
import com.atguigu.tingshu.user.mapper.UserPaidTrackMapper;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.album.TrackListVo;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import com.atguigu.tingshu.vo.user.UserCollectVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.atguigu.tingshu.vo.user.UserSubscribeVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service
//@SuppressWarnings这个注解的作用是抑制警告信息，比如：unchecked,rawtypes等
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {

	@Autowired
	private UserInfoMapper userInfoMapper;

	@Autowired
	private WxMaService wxMaService;

	@Autowired
	private RsaSigner rsaSigner;

	@Autowired
	private StringRedisTemplate redisTemplate;

	@Autowired
	private MongoTemplate mongoTemplate;

	@Autowired
	private RabbitService rabbitService;

	@Autowired
	private UserPaidTrackMapper userPaidTrackMapper;

	@Autowired
	private UserPaidAlbumMapper userPaidAlbumMapper;

	@Autowired
	private TrackInfoFeignClient trackInfoFeignClient;


	/**
	 * 微信登录功能的实现
	 * @param code 临时登录凭证,我们根据这个code可以和微信服务器交互获取openId
	 * @return
	 */
	@Override
	@Transactional(rollbackFor = Exception.class)
	public HashMap<String, Object> wxLogin(String code) {
		String openId = "";
		//1.1健壮性判定
		if (StringUtils.isEmpty(code)){
			throw new GuiguException(201,"无法读取临时登录凭证");
		}
		//1.2 获取微信登录的客户端
		WxMaUserService userService = wxMaService.getUserService();
		//1.3 根据临时登录凭证，解析获取openId
        try {
			//获取微信客户端的会话对象
            WxMaJscode2SessionResult sessionInfo = userService.getSessionInfo(code);
			//根据会话对象获取openId
			openId = sessionInfo.getOpenid();
			if (StringUtils.isEmpty(openId)){
				throw new GuiguException(201,"openId不存在，登录失败");
			}
		} catch (WxErrorException e) {
            throw new GuiguException(201,"微信登录内部异常，登录失败");
        }

		//2.我们在进行登录流程验证的时候，要先去判断redis库中是否存在用户的refreshToken
		String refreshTokenKey = RedisConstant.USER_LOGIN_REFRESH_KEY_PREFIX + openId;
		String tokenFromRedis = redisTemplate.opsForValue().get(refreshTokenKey);
		//2.1 若当前redis中存在用户的refreshToken，那么我们就将token的map返回给客户端
		if (!StringUtils.isEmpty(tokenFromRedis)){
			HashMap<String, Object> map = new HashMap<>();
			map.put("token",tokenFromRedis);
			map.put("refreshToken",tokenFromRedis);
			return map;
		}


		//3.先去数据库中查询该openId的用户信息
		LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
		queryWrapper.eq(UserInfo::getWxOpenId,openId);
		UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
		//3.1 判断当前是否存在该用户，若不存在，那么就执行注册业务
		if (null == userInfo){
			//3.2 进行注册
			userInfo = new UserInfo();
			userInfo.setWxOpenId(openId);
			userInfo.setNickname("用户" + System.currentTimeMillis());
			userInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
			userInfo.setIsVip(0);
			userInfo.setGender(0);
			userInfo.setIntro("简洁空空如也");
			//3.3 保存用户信息到数据库中
			int insert = userInfoMapper.insert(userInfo);
			log.info("注册用户信息：{}", insert > 0 ? "success" : "fail");
			//3.4 进行异步处理，发送请求到mq，进行用户账号的初始化
			rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_INIT_ACCOUNT, userInfo.getId().toString());
		}
		Long userId = userInfo.getId();
		//4.创建token，并且将token保存到redis中
		String jsonWebToken = getJsonWebToken(openId, userId);
		/**
		 * 此处我们选择使用双令牌机制保存token，即accessToken和refreshToken
		 * 其中accessToken的过期时间短，refreshToken的过期时间长
		 * 我们选择使用双令牌机制的目的是：提高用户的体验，防止用户需要频繁的进行登录操作，同时也保障安全性
		 * 如果我们设置的token过期时间过短，那么就会导致：用户需要频繁重新登录，用户体验差
		 * 如果我们设置的token过期时间过长，那么就会导致：token令牌的存活时间过长，期间容易被攻击获取令牌，造成安全隐患
		 * 因此我们选择使用双令牌机制，在保障用户体验的同时，也保障了安全性
		 * 双令牌机制的具体实现思路：
		 * 1.我们在用户登录成功后，同时生成两个token，一个叫accessToken，一个叫refreshToken，并且将二者都存放到redis当中。
		 * 2.用户向我们发起请求，并且只携带accessToken，如果accessToken没有过期，并且经过解析，且成功，那么就正常访问。
		 * 3.如果用户此时的accessToken过期了，我们会返回给客户端一个401错误码
		 * 4.客户端如果接收到401错误码，那么就会携带refreshToken(以accessToken的形式，放入请求头中)，去访问我们的另外一个接口(用于刷新令牌——即重新获取一组accessToken和refreshToken)
		 * 5.然后再将这刷新的一组新的accessToken和refreshToken存入我们的redis数据库中，并且将新的一组token响应给客户端
		 * 6.然后客户端会携带这次新的accessToken来访问我们的资源，这样我们就实现了——“[无感知登录]”
		 * 7.这样既能避免重复登录，提高用户的体验的同时，保证了令牌的安全性
		 */
		//4.1 将双令牌都保存到redis中
		String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openId;
//		String refreshTokenKey = RedisConstant.USER_LOGIN_REFRESH_KEY_PREFIX + openId;
		redisTemplate.opsForValue().set(accessTokenKey, jsonWebToken, 25, TimeUnit.DAYS);
		redisTemplate.opsForValue().set(refreshTokenKey, jsonWebToken, 30, TimeUnit.DAYS);


		HashMap<String, Object> map = new HashMap<>();
		map.put("token",jsonWebToken);
		map.put("refreshToken",jsonWebToken);
		return map;
	}

	/**
	 * 刷新用户的令牌并且返回
	 * ——用户实现双令牌机制，实现无感知登录
	 * @return
	 */
	@Override
	public Map<String, Object> refreshToken() {
		//1.获取与当前线程绑定的HTTP请求属性的对象
		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

		//2.根据请求的属性对象，尝试获取请求对象
		HttpServletRequest request = requestAttributes.getRequest();

		//3.根据请求对象，获取请求头中的数据
		String refreshToken = request.getHeader("token");

		//4.现在我们有了refreshToken这个令牌，我们就可以去解析这个令牌，获取jwt中的负荷部分，拿到openId和userId
		// 解析token令牌，decodeAndVerify(v1,v2) v1:token令牌  v2:解析token的公钥
		Jwt jwt = JwtHelper.decodeAndVerify(refreshToken, new RsaVerifier(PublicConstant.PUBLIC_KEY));

		//5.解析token令牌成功，获取到jwt令牌对象，现在可以根据这个jwt令牌对象，获取到claims，即负荷
		String claims = jwt.getClaims();

		//6.根据负荷，获取到openId和userId
		Map map = JSONObject.parseObject(claims, Map.class);

		//7.获取openId和userId
		String openId = (String) map.get("openId");
		Long userId = (Long) map.get("userId");

		//8.根据openId和userId我们就可以调用方法，去获取一个新的token
		String jsonWebToken = getJsonWebToken(openId, userId);

		//9.先去判断当前的refreshToken在redis当中有没有过期，如果在redis当中refreshToken都过期了，那么我们就真的要让用户重新登录了
		String refreshTokenKey = RedisConstant.USER_LOGIN_REFRESH_KEY_PREFIX + openId;
		String tokenFromRedis = redisTemplate.opsForValue().get(refreshTokenKey);
		if (StringUtils.isEmpty(tokenFromRedis)){
			throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
		} else {
			//若refreshToken在redis当中没有过期，那么我们就返回一对新的token,并且将这对token重新存入redis当中
			String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openId;
			redisTemplate.opsForValue().set(accessTokenKey, jsonWebToken, 25, TimeUnit.DAYS);
			redisTemplate.opsForValue().set(refreshTokenKey, jsonWebToken, 30, TimeUnit.DAYS);

			//返回带有token的map给前端
			HashMap<String, Object> resultMap = new HashMap<>();
			resultMap.put("token",jsonWebToken);
			resultMap.put("refreshToken",jsonWebToken);
			return resultMap;
		}
	}

	/**
	 * 登录成功后 ，获取登录成功的用户的信息
	 * @return
	 */
	@Override
	public UserInfoVo getUserInfo() {
		//1.从ThreadLocal中获取userId
		Long userId = AuthContextHolder.getUserId();

		// 2.查询用户表
		UserInfo userInfo = userInfoMapper.selectById(userId);
		if (null == userInfo) {
			throw new GuiguException(201, "用户信息不存在");
		}

		// 3.将UserInfo转成UserInfoVo对象返回
		UserInfoVo userInfoVo = new UserInfoVo();
		BeanUtils.copyProperties(userInfo, userInfoVo); // 两个对象的属性名 两个对象的属性类型是否一致

		return userInfoVo;
	}

	/**
	 * 修改用户信息
	 * @param userInfoVo 用户的修改对象vo
	 */
	@Override
	public void updateUser(UserInfoVo userInfoVo) {
		//从ThreadLocal中获取userId
		Long userId = AuthContextHolder.getUserId();
		UserInfo userInfo = userInfoMapper.selectById(userId);
		if (null == userInfo){
			throw new GuiguException(201,"目标用户信息不存在");
		}
		userInfo.setNickname(userInfoVo.getNickname());
		userInfo.setAvatarUrl(userInfoVo.getAvatarUrl());
		userInfoMapper.updateById(userInfo);
	}

	/**
	 * [rpc]
	 * 远程调用，根据用户id获取用户信息
	 * @param userId 用户id
	 * @return 用户信息
	 */
	@Override
	public UserInfoVo getUserInfoVoByUserId(Long userId) {
		UserInfoVo vo = new UserInfoVo();
		UserInfo userInfo = userInfoMapper.selectById(userId);
		BeanUtils.copyProperties(userInfo,vo);
		return vo;
	}

	/**
	 * [rpc]
	 * 根据用户id和专辑id，获取用户购买过的音频数据集合
	 * @param userId 用户id
	 * @param albumId 专辑id
	 * @return 用户购买过的音频数据集合
	 */
	@Override
	public List<UserPaidTrack> getUserPaidTrackList(Long userId, Long albumId) {
        return userPaidTrackMapper.selectList(new LambdaQueryWrapper<UserPaidTrack>()
                .eq(UserPaidTrack::getUserId, userId)
                .eq(UserPaidTrack::getAlbumId, albumId)
                .eq(UserPaidTrack::getIsDeleted, 0));
	}


	/**
	 * 根据用户id和专辑id，获取用户购买过的专辑信息集合
	 * @param userId 用户id
	 * @param albumId 专辑id
	 * @return 用户购买过的音频数据集合
	 */
	@Override
	public List<UserPaidAlbum> getUserPaidAlbumList(Long userId, Long albumId) {
		return userPaidAlbumMapper.selectList(new LambdaQueryWrapper<UserPaidAlbum>()
				.eq(UserPaidAlbum::getUserId, userId)
				.eq(UserPaidAlbum::getAlbumId, albumId)
				.eq(UserPaidAlbum::getIsDeleted, 0));
	}


	/**
	 * 根据音频id获取当前用户是否收藏了这条音频
	 * @param trackId 音频id
	 * @return 是否收藏
	 */
	@Override
	public Boolean isCollect(Long trackId) {
		Long userId = AuthContextHolder.getUserId();

		//构建请求，从mongodb中查询是否收藏
		Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(trackId);
		Query query = new Query(criteria);

		//获取查询的集合名
		String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);

		//调用mongo，发起查询请求
		long count = mongoTemplate.count(query, UserCollect.class, collectionName);

		return count > 0;
	}


	/**
	 * 当前用户点击收藏按钮触发的接口
	 * @param trackId 音频id
	 * @return
	 */
	@Override
	public Boolean collect(Long trackId) {
		//注意! 收藏音频和取消收藏音频访问的都是这同一个接口
		//我们将用户收藏音频的数据存在mongodb当中，因此我们要先去mongo中查询指定音频id和用户id的数据
		//通过mongo的数据来判断是收藏操作还是取消收藏操作
		Long userId = AuthContextHolder.getUserId();
		String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);
		Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(trackId);
		Query query = new Query(criteria);

		long count = mongoTemplate.count(query, UserCollect.class, collectionName);

		//如果count = 0，那么就说明当前用户执行的是收藏操作
		if (count == 0){
			UserCollect userCollect = new UserCollect();
			userCollect.setUserId(userId);
			userCollect.setTrackId(trackId);
			userCollect.setCreateTime(new Date());
			//将收藏记录存入mongo
			mongoTemplate.save(userCollect,collectionName);
			//使用RabbitMQ，调用消息中间件，发消息给指定消费者去更新音频的收藏统计数
			TrackStatMqVo trackStatMqVo = parseTrackStatMqVo(trackId,1);
			rabbitService.sendMessage(MqConst.EXCHANGE_TRACK,MqConst.ROUTING_TRACK_STAT_UPDATE,JSONObject.toJSONString(trackStatMqVo));

			return true;
		} else {
			//否则的话，那么就是执行取消收藏操作 将收藏记录从mongo中移除
			mongoTemplate.remove(query,collectionName);
			//使用RabbitMQ，调用消息中间件，发消息给指定消费者去更新音频的收藏统计数
			TrackStatMqVo trackStatMqVo = parseTrackStatMqVo(trackId,-1);
			rabbitService.sendMessage(MqConst.EXCHANGE_TRACK,MqConst.ROUTING_TRACK_STAT_UPDATE,JSONObject.toJSONString(trackStatMqVo));

			return false;
		}

	}

	/**
	 * 根据当前用户id和专辑id判断当前用户是否订阅当前专辑
	 * @param albumId 专辑id
	 * @return 当前用户是否订阅专辑
	 */
	@Override
	public Boolean isSubscribe(Long albumId) {
		//1.获取当前用户的id
		Long userId = AuthContextHolder.getUserId();
		//2.获取mongo需要查询的集合名称
		String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_SUBSCRIBE, userId);
		//3.组合查询条件
		Criteria criteria = Criteria.where("userId").is(userId).and("albumId").is(albumId);
		Query query = new Query(criteria);
		//4.去mongo中查询订阅记录
		long count = mongoTemplate.count(query, UserSubscribe.class, collectionName);
		return count > 0;
	}


	/**
	 * 根据用户id展示用户分页收藏列表
	 * @param page 分页封装数据
	 * @param userId 用户id
	 * @return 分页收藏列表
	 */
	@Override
	public IPage<UserCollectVo> findUserCollectPage(IPage<UserCollectVo> page, Long userId) {

		//1.获取mongo操作集合名
		String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);

		//2.构建请求，去mongo中查询指定user_id的收藏列表
		//2.1 构建userId查询条件
		Criteria criteria = Criteria.where("userId").is(userId);
		Query query = new Query(criteria);

		//2.2 构建分页查询条件  因为我们使用的是mongo的查询，不是mybatis-plus的查询，所以要手动分页
		//同样的，我们要自己手动组装IPage对象，组装其中的total属性，records属性等等
		long currentPageNum = page.getCurrent();
		long pageSize = page.getSize();
		long pageShowNum = (currentPageNum - 1) * pageSize;
		//并且我们按照更新时间进行排序
		Sort sort = Sort.by(Sort.Direction.DESC, "updateTime");
		PageRequest pageRequest = PageRequest.of((int) pageShowNum, (int) pageSize, sort);
		//为query查询条件跟上这个分页查询条件
		query.with(pageRequest);

		//3.去mongo中指定的集合中根据这个查询条件进行查询
		//查询指定的用户收藏集合列表对象
		List<UserCollect> userCollectList = mongoTemplate.find(query, UserCollect.class, collectionName);

		//查询用户收藏集合列表的总个数
		long total = mongoTemplate.count(query, UserCollect.class, collectionName);

		//但是前端需要我们给的收藏集合列表对象是List<UserCollectVo>,我们需要进一步操作
		//UserCollectVo中有，但是UserCollect没有的属性有albumId，trackTitle，coverUrl
		//4.我们现在尝试获取trackID List，根据音频id集合去查询出对应的音频数据对象，然后获取封面url，音频标题和专辑id
		List<Long> trackIdList = userCollectList.stream().map(UserCollect::getTrackId).toList();

		if (!CollectionUtils.isEmpty(trackIdList)){
			Result<List<TrackListVo>> trackListVoListResult = trackInfoFeignClient.getTrackInfoListByIds(trackIdList);
			List<TrackListVo> trackListVoList = trackListVoListResult.getData();
			Assert.notNull(trackListVoList,"TrackInfoServiceImpl调用trackInfoFeignClient.getTrackInfoListByIds方法出错");

			//5.接下来我们获取了trackListVoList，我们接下来手动组装UserCollectVo集合
			//我们先把trackListVoList转换成Map
			Map<Long, TrackListVo> trackInfoMap = trackListVoList.stream().collect(Collectors.toMap(TrackListVo::getTrackId, v -> v));

			List<UserCollectVo> records = userCollectList.stream().map(collect -> {
				UserCollectVo userCollectVo = new UserCollectVo();

				Long trackId = collect.getTrackId();

				userCollectVo.setAlbumId(trackInfoMap.get(trackId).getAlbumId());
				userCollectVo.setTrackId(trackId);
				userCollectVo.setCreateTime(collect.getCreateTime());
				userCollectVo.setTrackTitle(trackInfoMap.get(trackId).getTrackTitle());
				userCollectVo.setCoverUrl(trackInfoMap.get(trackId).getCoverUrl());

				return userCollectVo;
			}).toList();

			return page.setRecords(records).setTotal(total);
		} else {
			return page;
		}

	}

	/**
	 * 将指定的音频id等信息封装成MQ消息对象，发送这个消息
	 * @param trackId 音频id
	 * @param i 操作收藏数的 操作数  1就是+1，那么就是收藏   -1就是-1，那么就是取消收藏
	 * @return 封装消息对象
	 */
	private TrackStatMqVo parseTrackStatMqVo(Long trackId, int i) {
		TrackStatMqVo trackStatMqVo = new TrackStatMqVo();
		trackStatMqVo.setBusinessNo(UUID.randomUUID().toString().replaceAll("-",""));
		//专辑没有收藏数这条统计数据，因此不用给专辑id
		//trackStatMqVo.setAlbumId();
		trackStatMqVo.setTrackId(trackId);
		trackStatMqVo.setStatType(SystemConstant.TRACK_STAT_COLLECT);
		trackStatMqVo.setCount(i);

		return trackStatMqVo;
	}


	/**
	 * 根据openId和userId来生成token
	 * @param openId 微信唯一标识符
	 * @param userId 用户id
	 * @return token
	 */
	private String getJsonWebToken(String openId, Long userId) {
		//首先创建一个负荷，也就是payload，用于封装信息，然后我们根据负荷来创建JWT令牌
		JSONObject payloadMap = new JSONObject();
		payloadMap.put("openId", openId);
		payloadMap.put("userId", userId);
		//使用JwtHelper来创建JWT令牌对象
		Jwt jwt = JwtHelper.encode(payloadMap.toString(),rsaSigner);
		//根据JWT对象来获取我们的token字符串
		String jsonWebToken = jwt.getEncoded();
		return jsonWebToken;
	}
}
